﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace Lwm.x10c.Assemble
{
    public class Assembler
    {
        private List<ushort> code = new List<ushort>();
        private List<ushort> extraWords = new List<ushort>();
        private Dictionary<string, ushort> definedLabels = new Dictionary<string, ushort>();
        private Dictionary<string, List<ushort>> unfixedLabels = new Dictionary<string, List<ushort>>();

        private ushort op;
        private int step = 0;

        private ArgsAssembler args;

        public Assembler()
        {
            this.args = new ArgsAssembler(this);
        }

        public ushort[] GetCode()
        {
            if (unfixedLabels.Count > 0)
                throw new Exception("Unresolved labels found!");

            return code.ToArray();
        }

        public void Label(string name)
        {
            ushort addr = (ushort)code.Count;
            this.definedLabels.Add(name, addr);

            List<ushort> fixes;
            if (unfixedLabels.TryGetValue(name, out fixes))
            {
                foreach (var fixLoc in fixes)
                {
                    code[fixLoc] = addr;
                }

                unfixedLabels.Remove(name);
            }
        }

        public void Jump(string label)
        {
            ushort labelPos;
            if (!definedLabels.TryGetValue(label, out labelPos))
            {
                labelPos = ushort.MaxValue;

                List<ushort> fixes;
                if (!unfixedLabels.TryGetValue(label, out fixes))
                    unfixedLabels[label] = fixes = new List<ushort>();

                fixes.Add((ushort)(code.Count + 1));
            }

            this.Set().PC().Literal(labelPos);
        }

        private ArgsAssembler Op(int code)
        {
            if (this.step != 0)
                throw new Exception("Invalid number of arguments. Check previous instruction.");

            this.step = 1;
            this.op = (ushort)code;
            return args;
        }

        public ArgsAssembler Set()
        {
            return Op(0x1);
        }

        public ArgsAssembler Add()
        {
            return Op(0x2);
        }

        public ArgsAssembler Sub()
        {
            return Op(0x3);
        }

        public ArgsAssembler Mul()
        {
            return Op(0x4);
        }

        public ArgsAssembler Div()
        {
            return Op(0x5);
        }

        public ArgsAssembler Mod()
        {
            return Op(0x6);
        }

        public ArgsAssembler Shl()
        {
            return Op(0x7);
        }

        public ArgsAssembler Shr()
        {
            return Op(0x8);
        }

        public ArgsAssembler And()
        {
            return Op(0x9);
        }

        public ArgsAssembler Or()
        {
            return Op(0xa);
        }

        public ArgsAssembler Xor()
        {
            return Op(0xb);
        }

        public ArgsAssembler Ife()
        {
            return Op(0xc);
        }

        public ArgsAssembler Ifn()
        {
            return Op(0xd);
        }

        public ArgsAssembler Ifg()
        {
            return Op(0xe);
        }

        public ArgsAssembler Ifb()
        {
            return Op(0xf);
        }

        internal ArgsAssembler Arg(int code, params ushort[] extra)
        {
            this.extraWords.AddRange(extra);

            if (step == 1)
            {
                op = (ushort)(op | (code << 4));
            }
            else if (step == 2)
            {
                op = (ushort)(op | (code << 10));
            }
            else
            {
                throw new Exception("Invalid number of arguments. Check previous instruction.");
            }
            step++;

            if (step > 2)
            {
                this.code.Add(op);
                this.code.AddRange(extraWords);
                this.extraWords.Clear();
                step = 0;
            }

            return args;
        }
    }
}
